{
  bashInteractive,
  buildPackages,
  cacert,
  callPackage,
  closureInfo,
  coreutils,
  devShellTools,
  e2fsprogs,
  proot,
  fakeNss,
  fakeroot,
  file,
  go,
  jq,
  jshon,
  lib,
  makeWrapper,
  moreutils,
  nix,
  nixosTests,
  pigz,
  rsync,
  runCommand,
  runtimeShell,
  shadow,
  skopeo,
  stdenv,
  storeDir ? builtins.storeDir,
  symlinkJoin,
  tarsum,
  util-linux,
  vmTools,
  writeClosure,
  writeScript,
  writeShellScriptBin,
  writeText,
  writeTextDir,
  writePython3,
  zstd,
}:

let
  inherit (lib)
    optionals
    optionalString
    ;

  inherit (lib)
    escapeShellArgs
    toList
    ;

  inherit (devShellTools)
    valueToString
    ;

  mkDbExtraCommand =
    contents:
    let
      contentsList = if builtins.isList contents then contents else [ contents ];
    in
    ''
      echo "Generating the nix database..."
      echo "Warning: only the database of the deepest Nix layer is loaded."
      echo "         If you want to use nix commands in the container, it would"
      echo "         be better to only have one layer that contains a nix store."

      export NIX_REMOTE=local?root=$PWD
      # A user is required by nix
      # https://github.com/NixOS/nix/blob/9348f9291e5d9e4ba3c4347ea1b235640f54fd79/src/libutil/util.cc#L478
      export USER=nobody
      ${buildPackages.nix}/bin/nix-store --load-db < ${
        closureInfo { rootPaths = contentsList; }
      }/registration
      # Reset registration times to make the image reproducible
      ${buildPackages.sqlite}/bin/sqlite3 nix/var/nix/db/db.sqlite "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"

      mkdir -p nix/var/nix/gcroots/docker/
      for i in ${lib.concatStringsSep " " contentsList}; do
      ln -s $i nix/var/nix/gcroots/docker/$(basename $i)
      done;
    '';

  # The OCI Image specification recommends that configurations use values listed
  # in the Go Language document for GOARCH.
  # Reference: https://github.com/opencontainers/image-spec/blob/master/config.md#properties
  # For the mapping from Nixpkgs system parameters to GOARCH, we can reuse the
  # mapping from the go package.
  defaultArchitecture = go.GOARCH;

  compressors = {
    none = {
      ext = "";
      nativeInputs = [ ];
      compress = "cat";
      decompress = "cat";
    };
    gz = {
      ext = ".gz";
      nativeInputs = [ pigz ];
      compress = "pigz -p$NIX_BUILD_CORES -nTR";
      decompress = "pigz -d -p$NIX_BUILD_CORES";
    };
    zstd = {
      ext = ".zst";
      nativeInputs = [ zstd ];
      compress = "zstd -T$NIX_BUILD_CORES";
      decompress = "zstd -d -T$NIX_BUILD_CORES";
    };
  };

  compressorForImage =
    compressor: imageName:
    compressors.${compressor}
      or (throw "in docker image ${imageName}: compressor must be one of: [${toString (builtins.attrNames compressors)}]");

in
rec {
  examples = callPackage ./examples.nix {
    inherit
      buildImage
      buildLayeredImage
      fakeNss
      pullImage
      shadowSetup
      buildImageWithNixDb
      streamNixShellImage
      ;
  };

  tests = {
    inherit (nixosTests)
      docker-tools
      docker-tools-overlay
      # requires remote builder
      # docker-tools-cross
      ;
  };

  pullImage =
    let
      fixName = name: builtins.replaceStrings [ "/" ":" ] [ "-" "-" ] name;
    in
    lib.fetchers.withNormalizedHash { } (
      {
        imageName,
        # To find the digest of an image, you can use skopeo:
        # see doc/functions.xml
        imageDigest,
        outputHash,
        outputHashAlgo,
        os ? "linux",
        # Image architecture, defaults to the architecture of the `hostPlatform` when unset
        arch ? defaultArchitecture,
        # This is used to set name to the pulled image
        finalImageName ? imageName,
        # This used to set a tag to the pulled image
        finalImageTag ? "latest",
        # This is used to disable TLS certificate verification, allowing access to http registries on (hopefully) trusted networks
        tlsVerify ? true,

        name ? fixName "docker-image-${finalImageName}-${finalImageTag}.tar",
      }:

      runCommand name
        {
          inherit imageDigest;
          imageName = finalImageName;
          imageTag = finalImageTag;
          impureEnvVars = lib.fetchers.proxyImpureEnvVars;

          inherit outputHash outputHashAlgo;
          outputHashMode = "flat";

          nativeBuildInputs = [ skopeo ];
          SSL_CERT_FILE = "${cacert.out}/etc/ssl/certs/ca-bundle.crt";

          sourceURL = "docker://${imageName}@${imageDigest}";
          destNameTag = "${finalImageName}:${finalImageTag}";
        }
        ''
          skopeo \
            --insecure-policy \
            --tmpdir=$TMPDIR \
            --override-os ${os} \
            --override-arch ${arch} \
            copy \
            --src-tls-verify=${lib.boolToString tlsVerify} \
            "$sourceURL" "docker-archive://$out:$destNameTag" \
            | cat  # pipe through cat to force-disable progress bar
        ''
    );

  # We need to sum layer.tar, not a directory, hence tarsum instead of nix-hash.
  # And we cannot untar it, because then we cannot preserve permissions etc.
  inherit tarsum; # pkgs.dockerTools.tarsum

  # buildEnv creates symlinks to dirs, which is hard to edit inside the overlay VM
  mergeDrvs =
    {
      derivations,
      onlyDeps ? false,
    }:
    runCommand "merge-drvs"
      {
        inherit derivations onlyDeps;
      }
      ''
        if [[ -n "$onlyDeps" ]]; then
          echo $derivations > $out
          exit 0
        fi

        mkdir $out
        for derivation in $derivations; do
          echo "Merging $derivation..."
          if [[ -d "$derivation" ]]; then
            # If it's a directory, copy all of its contents into $out.
            cp -drf --preserve=mode -f $derivation/* $out/
          else
            # Otherwise treat the derivation as a tarball and extract it
            # into $out.
            tar -C $out -xpf $drv || true
          fi
        done
      '';

  # Helper for setting up the base files for managing users and
  # groups, only if such files don't exist already. It is suitable for
  # being used in a runAsRoot script.
  shadowSetup = ''
    export PATH=${shadow}/bin:$PATH
    mkdir -p /etc/pam.d
    if [[ ! -f /etc/passwd ]]; then
      echo "root:x:0:0::/root:${runtimeShell}" > /etc/passwd
      echo "root:!x:::::::" > /etc/shadow
    fi
    if [[ ! -f /etc/group ]]; then
      echo "root:x:0:" > /etc/group
      echo "root:x::" > /etc/gshadow
    fi
    if [[ ! -f /etc/pam.d/other ]]; then
      cat > /etc/pam.d/other <<EOF
    account sufficient pam_unix.so
    auth sufficient pam_rootok.so
    password requisite pam_unix.so nullok yescrypt
    session required pam_unix.so
    EOF
    fi
    if [[ ! -f /etc/login.defs ]]; then
      touch /etc/login.defs
    fi
  '';

  # Run commands in a virtual machine.
  runWithOverlay =
    {
      name,
      fromImage ? null,
      fromImageName ? null,
      fromImageTag ? null,
      diskSize ? 1024,
      buildVMMemorySize ? 512,
      preMount ? "",
      postMount ? "",
      postUmount ? "",
    }:
    vmTools.runInLinuxVM (
      runCommand name
        {
          preVM = vmTools.createEmptyImage {
            size = diskSize;
            fullName = "docker-run-disk";
            destination = "./image";
          };
          inherit fromImage fromImageName fromImageTag;
          memSize = buildVMMemorySize;

          nativeBuildInputs = [
            util-linux
            e2fsprogs
            jshon
            rsync
            jq
          ];
        }
        ''
          mkdir disk
          mkfs /dev/${vmTools.hd}
          mount /dev/${vmTools.hd} disk
          cd disk

          function dedup() {
            declare -A seen
            while read ln; do
              if [[ -z "''${seen["$ln"]:-}" ]]; then
                echo "$ln"; seen["$ln"]=1
              fi
            done
          }

          if [[ -n "$fromImage" ]]; then
            echo "Unpacking base image..."
            mkdir image
            tar -C image -xpf "$fromImage"

            if [[ -n "$fromImageName" ]] && [[ -n "$fromImageTag" ]]; then
              parentID="$(
                cat "image/manifest.json" |
                  jq -r '.[] | select(.RepoTags | contains([$desiredTag])) | rtrimstr(".json")' \
                    --arg desiredTag "$fromImageName:$fromImageTag"
              )"
            else
              echo "From-image name or tag wasn't set. Reading the first ID."
              parentID="$(cat "image/manifest.json" | jq -r '.[0].Config | rtrimstr(".json")')"
            fi

            # In case of repeated layers, unpack only the last occurrence of each
            cat ./image/manifest.json  | jq -r '.[0].Layers | .[]' | tac | dedup | tac > layer-list
          else
            touch layer-list
          fi

          # Unpack all of the parent layers into the image.
          lowerdir=""
          extractionID=0
          for layerTar in $(cat layer-list); do
            echo "Unpacking layer $layerTar"
            extractionID=$((extractionID + 1))

            mkdir -p image/$extractionID/layer
            tar -C image/$extractionID/layer -xpf image/$layerTar
            rm image/$layerTar

            find image/$extractionID/layer -name ".wh.*" -exec bash -c 'name="$(basename {}|sed "s/^.wh.//")"; mknod "$(dirname {})/$name" c 0 0; rm {}' \;

            # Get the next lower directory and continue the loop.
            lowerdir=image/$extractionID/layer''${lowerdir:+:}$lowerdir
          done

          mkdir work
          mkdir layer
          mkdir mnt

          ${lib.optionalString (preMount != "") ''
            # Execute pre-mount steps
            echo "Executing pre-mount steps..."
            ${preMount}
          ''}

          if [ -n "$lowerdir" ]; then
            mount -t overlay overlay -olowerdir=$lowerdir,workdir=work,upperdir=layer mnt
          else
            mount --bind layer mnt
          fi

          ${lib.optionalString (postMount != "") ''
            # Execute post-mount steps
            echo "Executing post-mount steps..."
            ${postMount}
          ''}

          umount mnt

          (
            cd layer
            cmd='name="$(basename {})"; touch "$(dirname {})/.wh.$name"; rm "{}"'
            find . -type c -exec bash -c "$cmd" \;
          )

          ${postUmount}
        ''
    );

  exportImage =
    {
      name ? fromImage.name,
      fromImage,
      fromImageName ? null,
      fromImageTag ? null,
      diskSize ? 1024,
    }:
    runWithOverlay {
      inherit
        name
        fromImage
        fromImageName
        fromImageTag
        diskSize
        ;

      postMount = ''
        echo "Packing raw image..."
        mkdir -p $out
        tar -C mnt --hard-dereference --sort=name --mtime="@$SOURCE_DATE_EPOCH" -cf $out/layer.tar .
      '';

      postUmount = ''
        mv $out/layer.tar .
        rm -rf $out
        mv layer.tar $out
      '';
    };

  # Create an executable shell script which has the coreutils in its
  # PATH. Since root scripts are executed in a blank environment, even
  # things like `ls` or `echo` will be missing.
  shellScript =
    name: text:
    writeScript name ''
      #!${runtimeShell}
      set -e
      export PATH=${coreutils}/bin:/bin
      ${text}
    '';

  # Create a "layer" (set of files).
  mkPureLayer =
    {
      # Name of the layer
      name,
      # JSON containing configuration and metadata for this layer.
      baseJson,
      # Files to add to the layer.
      copyToRoot ? null,
      # When copying the contents into the image, preserve symlinks to
      # directories (see `rsync -K`).  Otherwise, transform those symlinks
      # into directories.
      keepContentsDirlinks ? false,
      # Additional commands to run on the layer before it is tar'd up.
      extraCommands ? "",
      uid ? 0,
      gid ? 0,
    }:
    runCommand "docker-layer-${name}"
      {
        inherit baseJson extraCommands;
        contents = copyToRoot;
        nativeBuildInputs = [
          jshon
          rsync
          tarsum
        ];
      }
      ''
        mkdir layer
        if [[ -n "$contents" ]]; then
          echo "Adding contents..."
          for item in $contents; do
            echo "Adding $item"
            rsync -a${if keepContentsDirlinks then "K" else "k"} --chown=0:0 $item/ layer/
          done
        else
          echo "No contents to add to layer."
        fi

        chmod ug+w layer

        if [[ -n "$extraCommands" ]]; then
          (cd layer; eval "$extraCommands")
        fi

        # Tar up the layer and throw it into 'layer.tar'.
        echo "Packing layer..."
        mkdir $out
        tarhash=$(tar -C layer --hard-dereference --sort=name --mtime="@$SOURCE_DATE_EPOCH" --owner=${toString uid} --group=${toString gid} -cf - . | tee -p $out/layer.tar | tarsum)

        # Add a 'checksum' field to the JSON, with the value set to the
        # checksum of the tarball.
        cat ${baseJson} | jshon -s "$tarhash" -i checksum > $out/json

        # Indicate to docker that we're using schema version 1.0.
        echo -n "1.0" > $out/VERSION

        echo "Finished building layer '${name}'"
      '';

  # Make a "root" layer; required if we need to execute commands as a
  # privileged user on the image. The commands themselves will be
  # performed in a virtual machine sandbox.
  mkRootLayer =
    {
      # Name of the image.
      name,
      # Script to run as root. Bash.
      runAsRoot,
      # Files to add to the layer. If null, an empty layer will be created.
      # To add packages to /bin, use `buildEnv` or similar.
      copyToRoot ? null,
      # When copying the contents into the image, preserve symlinks to
      # directories (see `rsync -K`).  Otherwise, transform those symlinks
      # into directories.
      keepContentsDirlinks ? false,
      # JSON containing configuration and metadata for this layer.
      baseJson,
      # Existing image onto which to append the new layer.
      fromImage ? null,
      # Name of the image we're appending onto.
      fromImageName ? null,
      # Tag of the image we're appending onto.
      fromImageTag ? null,
      # How much disk to allocate for the temporary virtual machine.
      diskSize ? 1024,
      # How much memory to allocate for the temporary virtual machine.
      buildVMMemorySize ? 512,
      # Commands (bash) to run on the layer; these do not require sudo.
      extraCommands ? "",
    }:
    # Generate an executable script from the `runAsRoot` text.
    let
      runAsRootScript = shellScript "run-as-root.sh" runAsRoot;
      extraCommandsScript = shellScript "extra-commands.sh" extraCommands;
    in
    runWithOverlay {
      name = "docker-layer-${name}";

      inherit
        fromImage
        fromImageName
        fromImageTag
        diskSize
        buildVMMemorySize
        ;

      preMount = lib.optionalString (copyToRoot != null && copyToRoot != [ ]) ''
        echo "Adding contents..."
        for item in ${escapeShellArgs (map (c: "${c}") (toList copyToRoot))}; do
          echo "Adding $item..."
          rsync -a${if keepContentsDirlinks then "K" else "k"} --chown=0:0 $item/ layer/
        done

        chmod ug+w layer
      '';

      postMount = ''
        mkdir -p mnt/{dev,proc,sys,tmp} mnt${storeDir}

        # Mount /dev, /sys and the nix store as shared folders.
        mount --rbind /dev mnt/dev
        mount --rbind /sys mnt/sys
        mount --rbind ${storeDir} mnt${storeDir}

        # Execute the run as root script. See 'man unshare' for
        # details on what's going on here; basically this command
        # means that the runAsRootScript will be executed in a nearly
        # completely isolated environment.
        #
        # Ideally we would use --mount-proc=mnt/proc or similar, but this
        # doesn't work. The workaround is to setup proc after unshare.
        # See: https://github.com/karelzak/util-linux/issues/648
        unshare -imnpuf --mount-proc sh -c 'mount --rbind /proc mnt/proc && chroot mnt ${runAsRootScript}'

        # Unmount directories and remove them.
        umount -R mnt/dev mnt/sys mnt${storeDir}
        rmdir --ignore-fail-on-non-empty \
          mnt/dev mnt/proc mnt/sys mnt${storeDir} \
          mnt$(dirname ${storeDir})
      '';

      postUmount = ''
        (cd layer; ${extraCommandsScript})

        echo "Packing layer..."
        mkdir -p $out
        tarhash=$(tar -C layer --hard-dereference --sort=name --mtime="@$SOURCE_DATE_EPOCH" -cf - . |
                    tee -p $out/layer.tar |
                    ${tarsum}/bin/tarsum)

        cat ${baseJson} | jshon -s "$tarhash" -i checksum > $out/json
        # Indicate to docker that we're using schema version 1.0.
        echo -n "1.0" > $out/VERSION

        echo "Finished building layer '${name}'"
      '';
    };

  buildLayeredImage = lib.makeOverridable (
    {
      name,
      compressor ? "gz",
      meta ? { },
      ...
    }@args:
    let
      stream = streamLayeredImage (
        removeAttrs args [
          "compressor"
          "meta"
        ]
      );
      compress = compressorForImage compressor name;
    in
    runCommand "${baseNameOf name}.tar${compress.ext}" {
      inherit (stream) imageName;
      passthru = {
        inherit (stream) imageTag;
        inherit stream;
      };
      nativeBuildInputs = compress.nativeInputs;
      inherit meta;
    } "${stream} | ${compress.compress} > $out"
  );

  # 1. extract the base image
  # 2. create the layer
  # 3. add layer deps to the layer itself, diffing with the base image
  # 4. compute the layer id
  # 5. put the layer in the image
  # 6. repack the image
  buildImage = lib.makeOverridable (
    args@{
      # Image name.
      name,
      # Image tag, when null then the nix output hash will be used.
      tag ? null,
      # Parent image, to append to.
      fromImage ? null,
      # Name of the parent image; will be read from the image otherwise.
      fromImageName ? null,
      # Tag of the parent image; will be read from the image otherwise.
      fromImageTag ? null,
      # Files to put on the image (a nix store path or list of paths).
      copyToRoot ? null,
      # When copying the contents into the image, preserve symlinks to
      # directories (see `rsync -K`).  Otherwise, transform those symlinks
      # into directories.
      keepContentsDirlinks ? false,
      # Docker config; e.g. what command to run on the container.
      config ? null,
      # Image architecture, defaults to the architecture of the `hostPlatform` when unset
      architecture ? defaultArchitecture,
      # Optional bash script to run on the files prior to fixturizing the layer.
      extraCommands ? "",
      uid ? 0,
      gid ? 0,
      # Optional bash script to run as root on the image when provisioning.
      runAsRoot ? null,
      # Size of the virtual machine disk to provision when building the image.
      diskSize ? 1024,
      # Size of the virtual machine memory to provision when building the image.
      buildVMMemorySize ? 512,
      # Time of creation of the image.
      created ? "1970-01-01T00:00:01Z",
      # Compressor to use. One of: none, gz, zstd.
      compressor ? "gz",
      # Populate the nix database in the image with the dependencies of `copyToRoot`.
      includeNixDB ? false,
      # Deprecated.
      contents ? null,
      # Meta options to set on the resulting derivation.
      meta ? { },
    }:

    let
      checked =
        lib.warnIf (contents != null)
          "in docker image ${name}: The contents parameter is deprecated. Change to copyToRoot if the contents are designed to be copied to the root filesystem, such as when you use `buildEnv` or similar between contents and your packages. Use copyToRoot = buildEnv { ... }; or similar if you intend to add packages to /bin."
          lib.throwIf
          (contents != null && copyToRoot != null)
          "in docker image ${name}: You can not specify both contents and copyToRoot.";

      rootContents = if copyToRoot == null then contents else copyToRoot;

      baseName = baseNameOf name;

      # Create a JSON blob of the configuration. Set the date to unix zero.
      baseJson =
        let
          pure = writeText "${baseName}-config.json" (
            builtins.toJSON {
              inherit created config architecture;
              preferLocalBuild = true;
              os = "linux";
            }
          );
          impure =
            runCommand "${baseName}-config.json"
              {
                nativeBuildInputs = [ jq ];
                preferLocalBuild = true;
              }
              ''
                jq ".created = \"$(TZ=utc date --iso-8601="seconds")\"" ${pure} > $out
              '';
        in
        if created == "now" then impure else pure;

      compress = compressorForImage compressor name;

      # TODO: add the dependencies of the config json.
      extraCommandsWithDB =
        if includeNixDB then (mkDbExtraCommand rootContents) + extraCommands else extraCommands;

      layer =
        if runAsRoot == null then
          mkPureLayer {
            name = baseName;
            inherit
              baseJson
              keepContentsDirlinks
              uid
              gid
              ;
            extraCommands = extraCommandsWithDB;
            copyToRoot = rootContents;
          }
        else
          mkRootLayer {
            name = baseName;
            inherit
              baseJson
              fromImage
              fromImageName
              fromImageTag
              keepContentsDirlinks
              runAsRoot
              diskSize
              buildVMMemorySize
              ;
            extraCommands = extraCommandsWithDB;
            copyToRoot = rootContents;
          };
      result =
        runCommand "docker-image-${baseName}.tar${compress.ext}"
          {
            nativeBuildInputs = [
              jshon
              jq
              moreutils
            ]
            ++ compress.nativeInputs;
            # Image name must be lowercase
            imageName = lib.toLower name;
            imageTag = lib.optionalString (tag != null) tag;
            inherit fromImage baseJson;
            layerClosure = writeClosure [ layer ];
            passthru.buildArgs = args;
            passthru.layer = layer;
            passthru.imageTag =
              if tag != null then
                tag
              else
                lib.head (
                  lib.strings.splitString "-" (baseNameOf (builtins.unsafeDiscardStringContext result.outPath))
                );
            inherit meta;
          }
          ''
            ${lib.optionalString (tag == null) ''
              outName="$(basename "$out")"
              outHash=$(echo "$outName" | cut -d - -f 1)

              imageTag=$outHash
            ''}

            # Print tar contents:
            # 1: Interpreted as relative to the root directory
            # 2: With no trailing slashes on directories
            # This is useful for ensuring that the output matches the
            # values generated by the "find" command
            ls_tar() {
              for f in $(tar -tf $1 | xargs realpath -ms --relative-to=.); do
                if [[ "$f" != "." ]]; then
                  echo "/$f"
                fi
              done
            }

            mkdir image
            touch baseFiles
            baseEnvs='[]'
            if [[ -n "$fromImage" ]]; then
              echo "Unpacking base image..."
              tar -C image -xpf "$fromImage"

              # Store the layers and the environment variables from the base image
              cat ./image/manifest.json  | jq -r '.[0].Layers | .[]' > layer-list
              configName="$(cat ./image/manifest.json | jq -r '.[0].Config')"
              baseEnvs="$(cat "./image/$configName" | jq '.config.Env // []')"

              # Extract the parentID from the manifest
              if [[ -n "$fromImageName" ]] && [[ -n "$fromImageTag" ]]; then
                parentID="$(
                  cat "image/manifest.json" |
                    jq -r '.[] | select(.RepoTags | contains([$desiredTag])) | rtrimstr(".json")' \
                      --arg desiredTag "$fromImageName:$fromImageTag"
                )"
              else
                echo "From-image name or tag wasn't set. Reading the first ID."
                parentID="$(cat "image/manifest.json" | jq -r '.[0].Config | rtrimstr(".json")')"
              fi

              # Otherwise do not import the base image configuration and manifest
              chmod a+w image image/*.json
              rm -f image/*.json

              for l in image/*/layer.tar; do
                ls_tar $l >> baseFiles
              done
            else
              touch layer-list
            fi

            chmod -R ug+rw image

            mkdir temp
            cp ${layer}/* temp/
            chmod ug+w temp/*

            for dep in $(cat $layerClosure); do
              find $dep >> layerFiles
            done

            echo "Adding layer..."
            # Record the contents of the tarball with ls_tar.
            ls_tar temp/layer.tar >> baseFiles

            # Append nix/store directory to the layer so that when the layer is loaded in the
            # image /nix/store has read permissions for non-root users.
            # nix/store is added only if the layer has /nix/store paths in it.
            if [ $(wc -l < $layerClosure) -gt 1 ] && [ $(grep -c -e "^/nix/store$" baseFiles) -eq 0 ]; then
              mkdir -p nix/store
              chmod -R 555 nix
              echo "./nix" >> layerFiles
              echo "./nix/store" >> layerFiles
            fi

            # Get the files in the new layer which were *not* present in
            # the old layer, and record them as newFiles.
            comm <(sort -n baseFiles|uniq) \
                 <(sort -n layerFiles|uniq|grep -v ${layer}) -1 -3 > newFiles
            # Append the new files to the layer.
            tar -rpf temp/layer.tar --hard-dereference --sort=name --mtime="@$SOURCE_DATE_EPOCH" \
              --owner=0 --group=0 --no-recursion --verbatim-files-from --files-from newFiles

            echo "Adding meta..."

            # If we have a parentID, add it to the json metadata.
            if [[ -n "$parentID" ]]; then
              cat temp/json | jshon -s "$parentID" -i parent > tmpjson
              mv tmpjson temp/json
            fi

            # Take the sha256 sum of the generated json and use it as the layer ID.
            # Compute the size and add it to the json under the 'Size' field.
            layerID=$(sha256sum temp/json|cut -d ' ' -f 1)
            size=$(stat --printf="%s" temp/layer.tar)
            cat temp/json | jshon -s "$layerID" -i id -n $size -i Size > tmpjson
            mv tmpjson temp/json

            # Use the temp folder we've been working on to create a new image.
            mv temp image/$layerID

            # Add the new layer ID to the end of the layer list
            (
              cat layer-list
              # originally this used `sed -i "1i$layerID" layer-list`, but
              # would fail if layer-list was completely empty.
              echo "$layerID/layer.tar"
            ) | sponge layer-list

            # Create image json and image manifest
            imageJson=$(cat ${baseJson} | jq '.config.Env = $baseenv + .config.Env' --argjson baseenv "$baseEnvs")
            imageJson=$(echo "$imageJson" | jq ". + {\"rootfs\": {\"diff_ids\": [], \"type\": \"layers\"}}")
            manifestJson=$(jq -n "[{\"RepoTags\":[\"$imageName:$imageTag\"]}]")

            for layerTar in $(cat ./layer-list); do
              layerChecksum=$(sha256sum image/$layerTar | cut -d ' ' -f1)
              imageJson=$(echo "$imageJson" | jq ".history |= . + [{\"created\": \"$(jq -r .created ${baseJson})\"}]")
              # diff_ids order is from the bottom-most to top-most layer
              imageJson=$(echo "$imageJson" | jq ".rootfs.diff_ids |= . + [\"sha256:$layerChecksum\"]")
              manifestJson=$(echo "$manifestJson" | jq ".[0].Layers |= . + [\"$layerTar\"]")
            done

            imageJsonChecksum=$(echo "$imageJson" | sha256sum | cut -d ' ' -f1)
            echo "$imageJson" > "image/$imageJsonChecksum.json"
            manifestJson=$(echo "$manifestJson" | jq ".[0].Config = \"$imageJsonChecksum.json\"")
            echo "$manifestJson" > image/manifest.json

            # Store the json under the name image/repositories.
            jshon -n object \
              -n object -s "$layerID" -i "$imageTag" \
              -i "$imageName" > image/repositories

            # Make the image read-only.
            chmod -R a-w image

            echo "Cooking the image..."
            tar -C image --hard-dereference --sort=name --mtime="@$SOURCE_DATE_EPOCH" --owner=0 --group=0 --xform s:'^./':: -c . | ${compress.compress} > $out

            echo "Finished."
          '';

    in
    checked result
  );

  # Merge the tarballs of images built with buildImage into a single
  # tarball that contains all images. Running `docker load` on the resulting
  # tarball will load the images into the docker daemon.
  mergeImages =
    images:
    runCommand "merge-docker-images"
      {
        inherit images;
        nativeBuildInputs = [
          file
          jq
        ]
        ++ compressors.none.nativeInputs
        ++ compressors.gz.nativeInputs
        ++ compressors.zstd.nativeInputs;
      }
      ''
        mkdir image inputs
        # Extract images
        repos=()
        manifests=()
        last_image_mime="application/gzip"
        for item in $images; do
          name=$(basename $item)
          mkdir inputs/$name

          last_image_mime=$(file --mime-type -b $item)
          case $last_image_mime in
            "application/x-tar") ${compressors.none.decompress};;
            "application/zstd") ${compressors.zstd.decompress};;
            "application/gzip") ${compressors.gz.decompress};;
            *) echo "error: unexpected layer type $last_image_mime" >&2; exit 1;;
          esac < $item | tar -xC inputs/$name

          if [ -f inputs/$name/repositories ]; then
            repos+=(inputs/$name/repositories)
          fi
          if [ -f inputs/$name/manifest.json ]; then
            manifests+=(inputs/$name/manifest.json)
          fi
        done
        # Copy all layers from input images to output image directory
        cp -R --update=none inputs/*/* image/
        # Merge repositories objects and manifests
        jq -s add "''${repos[@]}" > repositories
        jq -s add "''${manifests[@]}" > manifest.json
        # Replace output image repositories and manifest with merged versions
        mv repositories image/repositories
        mv manifest.json image/manifest.json
        # Create tarball and gzip
        tar -C image --hard-dereference --sort=name --mtime="@$SOURCE_DATE_EPOCH" --owner=0 --group=0 --xform s:'^./':: -c . | (
          case $last_image_mime in
            "application/x-tar") ${compressors.none.compress};;
            "application/zstd") ${compressors.zstd.compress};;
            "application/gzip") ${compressors.gz.compress};;
            # `*)` not needed; already checked.
          esac
        ) > $out
      '';

  # Provide a /etc/passwd and /etc/group that contain root and nobody.
  # Useful when packaging binaries that insist on using nss to look up
  # username/groups (like nginx).
  # /bin/sh is fine to not exist, and provided by another shim.
  inherit fakeNss; # alias

  # This provides a /usr/bin/env, for shell scripts using the
  # "#!/usr/bin/env executable" shebang.
  usrBinEnv = runCommand "usr-bin-env" { } ''
    mkdir -p $out/usr/bin
    ln -s ${coreutils}/bin/env $out/usr/bin
  '';

  # This provides /bin/sh, pointing to bashInteractive.
  # The use of bashInteractive here is intentional to support cases like `docker run -it <image_name>`, so keep these use cases in mind if making any changes to how this works.
  binSh = runCommand "bin-sh" { } ''
    mkdir -p $out/bin
    ln -s ${bashInteractive}/bin/bash $out/bin/sh
  '';

  # This provides the ca bundle in common locations
  caCertificates = runCommand "ca-certificates" { } ''
    mkdir -p $out/etc/ssl/certs $out/etc/pki/tls/certs
    # Old NixOS compatibility.
    ln -s ${cacert}/etc/ssl/certs/ca-bundle.crt $out/etc/ssl/certs/ca-bundle.crt
    # NixOS canonical location + Debian/Ubuntu/Arch/Gentoo compatibility.
    ln -s ${cacert}/etc/ssl/certs/ca-bundle.crt $out/etc/ssl/certs/ca-certificates.crt
    # CentOS/Fedora compatibility.
    ln -s ${cacert}/etc/ssl/certs/ca-bundle.crt $out/etc/pki/tls/certs/ca-bundle.crt
  '';

  # Build an image and populate its nix database with the provided
  # contents. The main purpose is to be able to use nix commands in
  # the container.
  # Be careful since this doesn't work well with multilayer.
  # TODO: add the dependencies of the config json.
  buildImageWithNixDb = args: buildImage (args // { includeNixDB = true; });

  buildLayeredImageWithNixDb = args: buildLayeredImage (args // { includeNixDB = true; });

  # Arguments are documented in ../../../doc/build-helpers/images/dockertools.section.md
  streamLayeredImage = lib.makeOverridable (
    {
      name,
      tag ? null,
      fromImage ? null,
      contents ? [ ],
      config ? { },
      architecture ? defaultArchitecture,
      created ? "1970-01-01T00:00:01Z",
      mtime ? "1970-01-01T00:00:01Z",
      uid ? 0,
      gid ? 0,
      uname ? "root",
      gname ? "root",
      maxLayers ? 100,
      extraCommands ? "",
      fakeRootCommands ? "",
      enableFakechroot ? false,
      includeStorePaths ? true,
      includeNixDB ? false,
      passthru ? { },
      meta ? { },
      # Pipeline used to produce docker layers. If not set, popularity contest
      # algorithm is used. If set, maxLayers is ignored as the author of the
      # pipeline can use one of the available functions (like "limit_layers")
      # to control the amount of layers.
      # See: pkgs/build-support/flatten-references-graph/src/flatten_references_graph/pipe.py
      # for available functions, and it's test for how to use them.
      # WARNING!! this interface is highly experimental and subject to change.
      layeringPipeline ? null,
      # Enables debug logging for the layering pipeline.
      debug ? false,
    }:
    assert (
      lib.assertMsg (layeringPipeline == null -> maxLayers > 1)
        "the maxLayers argument of dockerTools.buildLayeredImage function must be greather than 1 (current value: ${toString maxLayers})"
    );
    assert (
      lib.assertMsg (enableFakechroot -> !stdenv.hostPlatform.isDarwin) ''
        cannot use `enableFakechroot` because `proot` is not portable to Darwin. Workarounds:
              - use `fakeRootCommands` with the restricted `fakeroot` environment
              - cross-compile your packages
              - run your packages in a virtual machine
              Discussion: https://github.com/NixOS/nixpkgs/issues/327311''
    );
    let
      baseName = baseNameOf name;

      streamScript = writePython3 "stream" { } ./stream_layered_image.py;
      baseJson = writeText "${baseName}-base.json" (
        builtins.toJSON {
          inherit config architecture;
          os = "linux";
        }
      );

      contentsList = if builtins.isList contents then contents else [ contents ];
      bind-paths = toString (
        map (path: "--bind=${path}:${path}!") [
          "/dev/"
          "/proc/"
          "/sys/"
          "${builtins.storeDir}/"
          "$NIX_BUILD_TOP"
          "$out/layer.tar"
        ]
      );

      # We store the customisation layer as a tarball, to make sure that
      # things like permissions set on 'extraCommands' are not overridden
      # by Nix. Then we precompute the sha256 for performance.
      customisationLayer = symlinkJoin {
        name = "${baseName}-customisation-layer";
        paths = contentsList;
        extraCommands = (lib.optionalString includeNixDB (mkDbExtraCommand contents)) + extraCommands;
        inherit fakeRootCommands;
        nativeBuildInputs = [
          fakeroot
        ]
        ++ optionals enableFakechroot [
          proot
        ];
        postBuild = ''
          mv $out old_out
          (cd old_out; eval "$extraCommands" )

          mkdir $out
          ${
            if enableFakechroot then
              ''
                proot -r $PWD/old_out ${bind-paths} --pwd=/ fakeroot bash -e -c '
                  if [ -e "$NIX_ATTRS_SH_FILE" ]; then . "$NIX_ATTRS_SH_FILE"; fi
                  source $stdenv/setup
                  eval "$fakeRootCommands"
                  tar \
                    --sort name \
                    --exclude=./dev \
                    --exclude=./proc \
                    --exclude=./sys \
                    --exclude=.${builtins.storeDir} \
                    --exclude=".$NIX_BUILD_TOP" \
                    --numeric-owner --mtime "@$SOURCE_DATE_EPOCH" \
                    --hard-dereference \
                    -cf $out/layer.tar .
                '
              ''
            else
              ''
                fakeroot bash -e -c '
                  if [ -e "$NIX_ATTRS_SH_FILE" ]; then . "$NIX_ATTRS_SH_FILE"; fi
                  source $stdenv/setup
                  cd old_out
                  eval "$fakeRootCommands"
                  tar \
                    --sort name \
                    --numeric-owner --mtime "@$SOURCE_DATE_EPOCH" \
                    --hard-dereference \
                    -cf $out/layer.tar .
                '
              ''
          }
          sha256sum $out/layer.tar \
            | cut -f 1 -d ' ' \
            > $out/checksum
        '';
      };

      closureRoots = optionals includeStorePaths [
        baseJson
        customisationLayer
      ];

      excludePaths = [
        baseJson
        customisationLayer
      ];

      layersJsonFile =
        if layeringPipeline == null then
          buildPackages.dockerAutoLayer {
            inherit
              closureRoots
              debug
              excludePaths
              fromImage
              maxLayers
              ;
          }
        else
          buildPackages.dockerMakeLayers {
            inherit closureRoots debug excludePaths;
            pipeline = layeringPipeline;
          };

      conf =
        runCommand "${baseName}-conf.json"
          {
            inherit
              fromImage
              created
              mtime
              uid
              gid
              uname
              gname
              layersJsonFile
              ;
            imageName = lib.toLower name;
            preferLocalBuild = true;
            passthru.imageTag =
              if tag != null then
                tag
              else
                lib.head (
                  lib.strings.splitString "-" (baseNameOf (builtins.unsafeDiscardStringContext conf.outPath))
                );
            nativeBuildInputs = [ jq ];
          }
          ''
            ${
              if (tag == null) then
                ''
                  outName="$(basename "$out")"
                  outHash=$(echo "$outName" | cut -d - -f 1)

                  imageTag=$outHash
                ''
              else
                ''
                  imageTag="${tag}"
                ''
            }

            # convert "created" and "mtime" to iso format
            if [[ "$created" != "now" ]]; then
                created="$(date -Iseconds -d "$created")"
            fi
            if [[ "$mtime" != "now" ]]; then
                mtime="$(date -Iseconds -d "$mtime")"
            fi

            jq '
              . + {
                "store_dir": $store_dir,
                "from_image": $from_image,
                "store_layers": $store_layers[0],
                "customisation_layer", $customisation_layer,
                "repo_tag": $repo_tag,
                "created": $created,
                "mtime": $mtime,
                "uid": $uid,
                "gid": $gid,
                "uname": $uname,
                "gname": $gname
              }
              ' --arg store_dir "${storeDir}" \
                --argjson from_image ${if fromImage == null then "null" else "'\"${fromImage}\"'"} \
                --slurpfile store_layers "$layersJsonFile" \
                --arg customisation_layer ${customisationLayer} \
                --arg repo_tag "$imageName:$imageTag" \
                --arg created "$created" \
                --arg mtime "$mtime" \
                --arg uid "$uid" \
                --arg gid "$gid" \
                --arg uname "$uname" \
                --arg gname "$gname" \
                ${baseJson} \
                  | tee $out
          '';

      result =
        runCommand "stream-${baseName}"
          {
            inherit conf;
            inherit (conf) imageName;
            inherit streamScript;
            preferLocalBuild = true;
            passthru = passthru // {
              inherit (conf) imageTag;
              inherit conf;
              inherit streamScript;

              # Distinguish tarballs and exes at the Nix level so functions that
              # take images can know in advance how the image is supposed to be used.
              isExe = true;
            };
            nativeBuildInputs = [ makeWrapper ];
            inherit meta;
          }
          ''
            makeWrapper $streamScript $out --add-flags $conf
          '';
    in
    result
  );

  # This function streams a docker image that behaves like a nix-shell for a derivation.
  #
  # Docs: doc/build-helpers/images/dockertools.section.md
  # Tests: nixos/tests/docker-tools-nix-shell.nix
  streamNixShellImage =
    {
      drv,
      name ? drv.name + "-env",
      tag ? null,
      uid ? 1000,
      gid ? 1000,
      # Default to `/build` instead of a non-existent `/homeless-shelter` for backwards compatibility.
      #
      # https://github.com/NixOS/nix/issues/6379
      homeDirectory ? "/build",
      shell ? bashInteractive + "/bin/bash",
      command ? null,
      run ? null,
    }:
    assert lib.assertMsg (!(drv.drvAttrs.__structuredAttrs or false))
      "streamNixShellImage: Does not work with the derivation ${drv.name} because it uses __structuredAttrs";
    assert lib.assertMsg (
      command == null || run == null
    ) "streamNixShellImage: Can't specify both command and run";
    let

      # A binary that calls the command to build the derivation
      builder = writeShellScriptBin "buildDerivation" ''
        exec ${lib.escapeShellArg (valueToString drv.drvAttrs.builder)} ${lib.escapeShellArgs (map valueToString drv.drvAttrs.args)}
      '';

      staticPath = "${dirOf shell}:${lib.makeBinPath [ builder ]}";

      # https://github.com/NixOS/nix/blob/2.32.0/src/nix/nix-build/nix-build.cc#L617-L651
      rcfile = writeText "nix-shell-rc" ''
        unset PATH
        dontAddDisableDepTrack=1
        # TODO: https://github.com/NixOS/nix/blob/2.32.0/src/nix/nix-build/nix-build.cc#L628
        [ -e $stdenv/setup ] && source $stdenv/setup
        PATH=${staticPath}:"$PATH"
        SHELL=${lib.escapeShellArg shell}
        BASH=${lib.escapeShellArg shell}
        set +e
        [ -n "$PS1" -a -z "$NIX_SHELL_PRESERVE_PROMPT" ] && PS1='\n\[\033[1;32m\][nix-shell:\w]\$\[\033[0m\] '
        if [ "$(type -t runHook)" = function ]; then
          runHook shellHook
        fi
        unset NIX_ENFORCE_PURITY
        shopt -u nullglob
        shopt -s execfail
        ${optionalString (command != null || run != null) ''
          ${optionalString (command != null) command}
          ${optionalString (run != null) run}
          exit
        ''}
      '';

      # https://github.com/NixOS/nix/blob/2.32.0/src/libstore/include/nix/store/globals.hh#L778-L788
      sandboxBuildDir = "/build";

      drvEnv =
        devShellTools.unstructuredDerivationInputEnv { inherit (drv) drvAttrs; }
        // devShellTools.derivationOutputEnv {
          outputList = drv.outputs;
          outputMap = drv;
        };

      # Environment variables set in the image
      envVars = {

        # Root certificates for internet access
        SSL_CERT_FILE = "${cacert}/etc/ssl/certs/ca-bundle.crt";
        NIX_SSL_CERT_FILE = "${cacert}/etc/ssl/certs/ca-bundle.crt";

        # https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/derivation-builder.cc#L1001-L1004
        # PATH = "/path-not-set";
        # Allows calling bash and `buildDerivation` as the Cmd
        PATH = staticPath;

        # https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/derivation-builder.cc#L1006-L1012
        HOME = homeDirectory;

        # https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/derivation-builder.cc#L1014-L1018
        NIX_STORE = storeDir;

        # https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/derivation-builder.cc#L1020-L1021
        # TODO: Make configurable?
        NIX_BUILD_CORES = "1";

      }
      // drvEnv
      // {

        # https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/derivation-builder.cc#L1035-L1037
        NIX_BUILD_TOP = sandboxBuildDir;

        # https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/derivation-builder.cc#L1039-L1040
        TMPDIR = sandboxBuildDir;
        TEMPDIR = sandboxBuildDir;
        TMP = sandboxBuildDir;
        TEMP = sandboxBuildDir;

        # https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/derivation-builder.cc#L1042-L1046
        PWD = sandboxBuildDir;

        # https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/derivation-builder.cc#L1079-L1082
        # We don't set it here because the output here isn't handled in any special way
        # NIX_LOG_FD = "2";

        # https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/derivation-builder.cc#L1084-L1085
        TERM = "xterm-256color";
      };

    in
    streamLayeredImage {
      inherit name tag;
      contents = [
        binSh
        usrBinEnv
        (fakeNss.override {
          # Allows programs to look up the build user's home directory.
          #
          # https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/linux-derivation-builder.cc#L409-L416
          #
          # This slightly differs, however, since we use the passed-in `homeDirectory` instead of `sandboxBuildDir`.
          # We're doing this because it is arguably a bug in Nix that `sandboxBuildDir` is used here.
          #
          # https://github.com/NixOS/nix/issues/6379
          extraPasswdLines = [
            "nixbld:x:${toString uid}:${toString gid}:Build user:${homeDirectory}:/noshell"
          ];
          extraGroupLines = [
            "nixbld:!:${toString gid}:"
          ];
        })
      ];

      fakeRootCommands = ''
        # Effectively a single-user installation of Nix, giving the user full
        # control over the Nix store. Needed for building the derivation this
        # shell is for, but also in case one wants to use Nix inside the image.
        mkdir -p ./nix/{store,var/nix} ./etc/nix
        chown -R ${toString uid}:${toString gid} ./nix ./etc/nix

        # Gives the user control over the build directory.
        mkdir -p .${sandboxBuildDir}
        chown -R ${toString uid}:${toString gid} .${sandboxBuildDir}
      '';

      # Run this image as the given uid/gid
      config.User = "${toString uid}:${toString gid}";
      config.Cmd =
        # https://github.com/NixOS/nix/blob/2.32.0/src/nix/nix-build/nix-build.cc#L240-L241
        # https://github.com/NixOS/nix/blob/2.32.0/src/nix/nix-build/nix-build.cc#L659
        if run == null then
          [
            shell
            "--rcfile"
            rcfile
          ]
        else
          [
            shell
            rcfile
          ];
      config.WorkingDir = sandboxBuildDir;
      config.Env = lib.mapAttrsToList (name: value: "${name}=${value}") envVars;
    };

  # Wrapper around `streamNixShellImage` to build an image from the result.
  #
  # Docs: doc/build-helpers/images/dockertools.section.md
  # Tests: nixos/tests/docker-tools-nix-shell.nix
  buildNixShellImage =
    {
      drv,
      compressor ? "gz",
      ...
    }@args:
    let
      stream = streamNixShellImage (removeAttrs args [ "compressor" ]);
      compress = compressorForImage compressor drv.name;
    in
    runCommand "${drv.name}-env.tar${compress.ext}" {
      inherit (stream) imageName;
      passthru = { inherit (stream) imageTag; };
      nativeBuildInputs = compress.nativeInputs;
    } "${stream} | ${compress.compress} > $out";
}
